package core;

import core.exception.InvalidConversionException;

/** 
 * Represents an amount of something, which is both a quantity and a unit
 *
 */
public class Amount extends ModelObject {

	private static final long serialVersionUID = 6920737024727957608L;

    /** The quantity, or number of units */
    private float _quantity;
    
    /** The unit of this amount */
    private UnitOfMeasurement _unit;
    
    /**
     * Constructs an Amount from two strings
     * @param quantity must be a float representing the quantity
     * @param unit must be a string that can be converted to a unit.
     * @throws IllegalArgumentException if the quantity is less than 0 or either string
     * cannot be converted.
     * 
     * @pre quantity >= 0 && unit != null
     * @post a valid Amount is constructed
     */   
    protected Amount(String quantity, String unit) throws IllegalArgumentException {
    	// Note: this method uses exceptions instead of asserts because it is in
    	// charge of trying to convert the strings which might fail. Part of the
    	// method's purpose is to alert the caller that the conversion can't be done
    	// and that isn't possible if an assert is used that crashes the whole program.
    	
    	if (quantity == null || unit == null)
    		throw new IllegalArgumentException("Null strings!");
		try {
			_quantity = Float.parseFloat(quantity);
			if (_quantity < 0)
				throw new IllegalArgumentException("quantity less than 0");
			
			unit = unit.replace(' ', '_'); // work around for fluid ounces
			_unit = UnitOfMeasurement.valueOf(unit);	
		}
		catch (NumberFormatException e){
			throw new IllegalArgumentException("Quantity was not a float");
		}	   	
    }
	
    /**
     * Constructs an Amount
     * @param quantity The number of units
     * @param unit The unit of measurement, which must not be an empty string
     * @throws IllegalArgumentException if the quantity is less than 0 or the unit is null
     * 
     * @pre quantity >= 0 && unit != null
     * @post a valid Amount is constructed
     */
	public Amount(float quantity, UnitOfMeasurement unit) throws IllegalArgumentException {
		assert(unit != null);
		assert(_quantity >= 0F);
		
		_quantity = quantity;
		_unit = unit;
	}
	
	/**
     * 
     * @return the number of units
     * 
     * @pre none.
     * @post returns a float >= 0.  The Amount is unchanged.
     */
	public float getQuantity() {
		return _quantity;
	}
	
	/**
	 * Sets the quantity of the unit
	 * 
	 * @param qty how many units in an item
	 * 
	 * @throws IllegalArgumentException thrown if quantity not positive
	 * 
	 * @pre qty >= 0.
	 * @post this._quantity = qty
	 */
	public void setQuantity(float qty) throws IllegalArgumentException{
		assert(qty >= 0F);
		
		_quantity = qty;
	}
	
	/**
	 * checks if the qty is valid
	 * 
	 * @param qty the quantity (how many units in an item) to be checked
	 * 
	 * @return returns true if qty is valid, else returns false
	 * 
	 * @pre qty is a float
	 * @post see return statement
	 */
	protected boolean canSetQuantity(float qty){
		return (qty >= 0F);
	}
	
	 /**
     * 
     * @return the unit of this Amount
     * 
     * @pre none.
     * @post returns the _unit of this Amount.  This Amount is unchanged.
     */
	public UnitOfMeasurement getUnit() {
		return _unit;
	}

	/**
	 * Sets the unit of measurement
	 * 
	 * @param unit the new unit of measurement
	 * 
	 * @pre unit != null
	 * @post this._unit = unit;
	 */
	protected void setUnitOfMeasurement(UnitOfMeasurement unit) {
		assert(unit != null);
		_unit = unit;
	}
	
	/**
	 * Add a given Amount to this Amount
	 * @param secondAmount the Amount to add
	 * @throws InvalidConversionException if the units of this Amount and the given Amount
	 * are incompatible with each other.
	 */
	public void add(Amount secondAmount) throws InvalidConversionException {
		//edit in place
		if(_unit == UnitOfMeasurement.count) {
			_quantity++;
			return;
		}
		
		Float conversion = _unit.convertFactorTo(secondAmount._unit);
		if (conversion == null) {
			throw new InvalidConversionException(getIllegalConversionMessage(secondAmount));
		}
		_quantity += (secondAmount._quantity * conversion.floatValue());
		
	}

	private String getIllegalConversionMessage(Amount secondAmount)
	{
		return "Can't convert " + _unit + " to " + secondAmount._unit + "!";
	}
	/**
	 * Subtract the given amount from this amount
	 * NOTE: sets quantity to 0 if subtracting a large amount than current amt
	 * 
	 * @param secondAmount the amount the subtract
	 * 
	 * @throws InvalidConversionException thrown if the units are not compatible
	 */
	public void subtract(Amount secondAmount) throws InvalidConversionException
	{
		Float conversion = _unit.convertFactorTo(secondAmount._unit);
		if (conversion == null) {
			throw new InvalidConversionException(getIllegalConversionMessage(secondAmount));
		}
		_quantity -= (secondAmount._quantity * conversion.floatValue());
		if (_quantity < 0)
			_quantity = 0;
	}
	
	/**
	 * Multiplies the quantity of amount by number
	 * 
	 * @param number the number to multiply by
	 * 
	 * @return a new amount with the modified multiplier
	 * 
	 * @pre number >= 0
	 */
	protected Amount multiply(float number)
	{
		assert (number >= 0);
		float newQty = _quantity*number;
		
		Amount newAmt = new Amount(newQty, _unit);
		return newAmt;
	}
	
	/**
	 * Divides the quantity of amount by the number
	 * 
	 * @param number the divisor
	 * 
	 * @return an Amount with same unit, but has quantity = quotient
	 */
	protected Amount divide(float number)
	{
		assert (number > 0);
		float newQty = _quantity/number;
		Amount newAmt = new Amount(newQty, _unit);
		return newAmt;
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = (int) (prime * result + _quantity);
		result = prime * result + ((_unit == null) ? 0 : _unit.hashCode());
		return result;
	}

	@Override	
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Amount other = (Amount) obj;
		if (_quantity != other._quantity)
			return false;
		if (_unit == null) {
			if (other._unit != null)
				return false;
		} else if (!_unit.equals(other._unit))
			return false;
		return true;
	}
	
	@Override
	public String toString() {
		
		StringBuilder sb = new StringBuilder();
		if (_quantity - (int)_quantity == 0) {
			sb.append((int)_quantity);
		}
		else {
			sb.append(_quantity);
		}
		sb.append(" ");
		sb.append(_unit.toString());
		return sb.toString();
	}
}
